package smarture;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;

import jnibwapi.JNIBWAPI;
import jnibwapi.model.Unit;
import jnibwapi.types.UnitType.UnitTypes;
import jnibwapi.types.WeaponType;

public class Vulture {
	private final int ACTION_ATTACK = 0;
	private final int ACTION_RETREAT = 1;
	
	private final JNIBWAPI bwapi;
    private final HashSet<Unit> enemyUnits;
    private final Unit unit;
    private final int attackRange;
    private final int maxHealth;
    
    private final int enemyAttackRange;
    private final double enemySpeed;
       
    private HashMap<Integer, HashMap<QState,Double> > Q;
    private QState lastState;
    private int lastAction;
    
    private int oldHealth;
    private ArrayList<Integer> oldEnemyHealth;
    private int stateCounter;
    
    public Vulture(Unit unit, JNIBWAPI bwapi, HashSet<Unit> enemyUnits) {
        this.unit = unit;
        this.bwapi = bwapi;
        this.enemyUnits = enemyUnits;
        
        this.attackRange = bwapi.getWeaponType(WeaponType.WeaponTypes.Fragmentation_Grenade.getID()).getMaxRange();
        this.enemyAttackRange = bwapi.getWeaponType(WeaponType.WeaponTypes.Psi_Blades.getID()).getMaxRange();
        this.maxHealth = unit.getHitPoints();
        this.enemySpeed = this.bwapi.getUnitType(UnitTypes.Protoss_Zealot.getID()).getTopSpeed();
                
        if( !loadQ() )
        {
        	this.Q = new HashMap<Integer, HashMap<QState,Double> >();
        	this.Q.put(ACTION_ATTACK, new HashMap<QState,Double>() );
        	this.Q.put(ACTION_RETREAT, new HashMap<QState,Double>() );
        	System.out.println("new Q created");
        }
        
        this.lastAction = -1;
        this.oldEnemyHealth = new ArrayList<Integer>();
    }
    
    public void step()
    {
    	if( lastAction == ACTION_ATTACK && unit.getGroundWeaponCooldown() == 0 )
    		return;
    	
    	Unit target = getClosestTarget();
    	if( target == null )
    		return;
    	
    	// get current state
    	QState currentState = calcQState();
    	// get new enemy hitpoints
    	ArrayList<Integer> enemyHealth = new ArrayList<Integer>();
    	for( Unit u : enemyUnits )
    	{
    		enemyHealth.add(u.getHitPoints());
    	}
    	// check if we already did an action
    	if( lastState != null )
    	{
	    	Double Qt = this.Q.get(lastAction).get(lastState);
	    	if( Qt == null )
	    		Qt = 0.0;
	    	
	    	double alpha = 0.05;
	    	double gamma = 0.9;
	    	// r_t+1
	    	double reward = Utils.calculateReward(this.oldHealth, unit.getHitPoints(), this.oldEnemyHealth, enemyHealth);
	    	//System.out.println("Rew: "+reward);
	    	// maxQt( s_t1, a )
	    	Double QtMax1 = Q.get(ACTION_ATTACK).get(currentState);
	    	Double QtMax2 = Q.get(ACTION_RETREAT).get(currentState);
	    	if( QtMax1 == null ) QtMax1 = 0.0;
	    	if( QtMax2 == null ) QtMax2 = 0.0;
	    		
	    	Double QtMax = Math.max(QtMax1, QtMax2);
	    	//  Q_t+1 = Q_t( s_t, a_t ) + alpha ( r_t+1 + gamma * maxQt( s_t1, a ) - Q_t( s_t, a_t ) )
	    	Qt = Qt + alpha * (reward + gamma * QtMax - Qt);
	    	// Update Q_t+1
	    	this.Q.get(lastAction).put(lastState, Qt);
	    	//System.out.println("old value: "+this.Q.get(lastAction).get(lastState));
    	}
    	this.oldEnemyHealth = enemyHealth;
    	
    	//System.out.println(currentState.toString());
    	
    	
    	Double r1 = this.Q.get(ACTION_ATTACK).get(currentState);
    	Double r2 = this.Q.get(ACTION_RETREAT).get(currentState);
    	//System.out.println("r1: "+r1+ " <> r2: "+r2+ " | lastaction: "+lastAction);
    	
    	int currentAction;
    	
    	if( (r1 == null && r2 == null) || (r1 == null && r2 != null) || (r1 != null && r2 != null && r1 > r2) )
    	{
    		currentAction = ACTION_ATTACK;
    	}
    	else
    	{
    		currentAction = ACTION_RETREAT;
    	}
    	
    	if( currentAction == lastAction )
    	{
    		stateCounter++;
    		if( stateCounter > 10 )
    			currentAction = currentAction == ACTION_ATTACK ? ACTION_RETREAT : ACTION_ATTACK;
    	}
    	else
    		stateCounter = 0;
    	
    	if( currentAction == ACTION_ATTACK )
    	{
    		//System.out.println("Attack");
    		attack();
    		lastAction = ACTION_ATTACK;
    	}
    	else
    	{
    		//System.out.println("Retreat");
    		retreat();
    		lastAction = ACTION_RETREAT;
    	}
    	
    	lastState = currentState;
    }
    
    private QState calcQState()
    {
    	Unit closestEnemy = this.getClosestTarget();
	
    	int distanceState;
    	double d = getDistance(closestEnemy);
    	//d = (this.enemySpeed * this.tickDiff) / d;
    	d = d / attackRange;
    	if( d >= 0.85 )
    		distanceState = 3;
    	else if( d >= 0.55 )
    		distanceState = 2;
    	else if( d >= 0.35 )
    		distanceState = 1;
    	else
    		distanceState = 0;
    	
    	List<Unit> units = this.getTargetsInRange(this.attackRange);
    	
    	int healthState;
    	double health = unit.getHitPoints()/this.maxHealth;
    	if( health >= 0.75 )
    		healthState = 3;
    	else if( health >= 0.5 )
    		healthState = 2;
    	else if( health >= 0.25 )
    		healthState = 1;
    	else
    		healthState = 0;
    	
    	return new QState(unit.getGroundWeaponCooldown(),distanceState,units.size(),healthState);
    }
    
    private void attack()
    {
    	Unit target = null;
    	int lowestHealth = Integer.MAX_VALUE;
    	
    	for( Unit u: this.getTargetsInRange(Integer.MAX_VALUE) )
    	{
    		int health = u.getHitPoints();
    		if( target == null || health < lowestHealth )
    		{
    			target = u;
    			lowestHealth = health;
    		}
    	}
    	if( target != null )
    		bwapi.attack(unit.getID(), target.getID());
    }
    
    private boolean retreat( )
    {
    	double[] target = {0,0};
    	for( Unit u : enemyUnits )
    	{
    		if( getDistance(u) < (this.enemyAttackRange+this.enemySpeed*50) )
    		{
    			target[0] -= (u.getX()-unit.getX());
    			target[1] -= (u.getY()-unit.getY());
    		}
    	}
    	
    	if( target[0] != 0 || target[1] != 0 )
    	{
    		bwapi.move(unit.getID(), (int)(unit.getX()+target[0]), (int)(unit.getY()+target[1]));
    		return true;
    	}
    	return false;
    }
    
    
    private Unit getClosestTarget()
    {
    	Unit result = null;
    	double bestDistance = Double.MAX_VALUE;
    	for( Unit u: enemyUnits )
    	{
    		double distance = getDistance(u);
    		if( result == null || distance < bestDistance )
    		{
    			result = u;
    			bestDistance = distance;
    		}
    	}
    	return result;
    }
    
    List<Unit> getTargetsInRange( double range )
    {
    	List<Unit> result = new LinkedList<Unit>();
    	for( Unit u: enemyUnits )
    	{
    		if( getDistance(u) <= range )
    			result.add(u);
    	}
    	return result;
    }
    
    private double getDistance(Unit u)
    {
    	return Math.sqrt( Math.pow(u.getX()-unit.getX(),2) + Math.pow(u.getY()-unit.getY(),2) );
    }
    
    public int getID()
    {
    	return unit.getID();
    }
    
    public void saveQ()
    {
    	try (
	      OutputStream file = new FileOutputStream("Q.obj");
	      OutputStream buffer = new BufferedOutputStream(file);
	      ObjectOutput output = new ObjectOutputStream(buffer);
	    )
	    {
	      output.writeObject(Q);
	      System.out.println("Q saved");
	    }  
	    catch(IOException ex){
	    	System.out.println(ex.getMessage());
	    }
    }
    
    @SuppressWarnings("unchecked")
	public boolean loadQ()
    {
    	try(
	      InputStream file = new FileInputStream("Q.obj");
	      InputStream buffer = new BufferedInputStream(file);
	      ObjectInput input = new ObjectInputStream (buffer);
	    ){
	      this.Q = (HashMap<Integer, HashMap<QState,Double> >)input.readObject();
	      if ( this.Q != null )
	      {
	    	  System.out.println("Q loaded");
	    	  return true;
	      }
	    	  
	    }
	    catch(ClassNotFoundException ex){
	    	System.out.println(ex.getMessage());
	    }
	    catch(IOException ex){
	    	System.out.println(ex.getMessage());
	    }
    	return false;
    }
    
    public int getHealth()
    {
    	return unit.getHitPoints();
    }

}
